#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#

import os,os.path,stat,sys,time
if sys.version_info[:3] < (2,6,0) :
    import statvfs

from bisect import bisect_left,insort

from celtuce.response import Response, RC_OK,RC_TABLE_NOT_FOUND,RC_CELL_NOT_FOUND,RC_WORKER_NOT_FOUND,RC_NOT_FOUND,RC_TIMEOUT
from worker import Worker
from celtuce.event import Event, OP_SHUTDOWN, OP_SPLIT_CELL, OP_ASSIGN_CELL
from celtuce.event import EVENT_NEW_CELL_DEVICE,EVENT_NEW_CELL_BORN,ROUTE_ADD_CELL,ROUTE_DEL_CELL
from consts import CELL_VOLUME,CELL_CONFIG_FILE

from cell import Cell
from celler import Celler
from consts import TIMEOUT_WAIT_REPLY
from helper import loadCellConfig

import celtuce.help.logging as logging


class   Keeper(Celler) :
    def __init__(self, path_list) :
        Celler.__init__(self)

        if isinstance(path_list, str) :
            _path_list = path_list.split(':')
            self._path_list = _path_list
        else :    
            self._path_list = path_list

        self._devices = {}
        self._workers = {}
        
        self._table_cells = {}

        self._CELL_VOLUME = CELL_VOLUME

        #self._queue_sync = queue.Queue(0)
        self._logger = logging.getLogger('keeper')
        ## __init__()

    def shutdown(self) :
        # stop handler of workers
        event_shutdown = Event()
        event_shutdown.request_id = 'shutdown'
        event_shutdown.op = OP_SHUTDOWN
        self._logger.info('shutdown workers ...')
        for id,worker in self._workers.iteritems() :
            while worker.isAlive() :
                worker.handle(event_shutdown)
                time.sleep(0.01)
            worker.stop()
        self._workers = {}

        # stop handler of keeper
        self._logger.info('shutdown keeper ...')
        self.stop()
        ## shutdown()

    def startup(self) :
        # STEP. scan devices
        self.scanDevices()

        # STEP. start handler
        self.start()

        ## startup()

    def scanDevices(self) :
        # get device id
        for path in self._path_list :
            dev_id = Device.getDeviceID(path)
            if dev_id in self._devices :
                _dev =self._devices[ dev_id ]
                _dev.path_list.append( path )
            else :
                _dev = Device()
                _dev.dev_id = dev_id
                _dev.path_list.append( path )
                self._devices[ dev_id ] = _dev

            _worker = self.__getWorker( dev_id )

            cells = self.scanCells(path, dev_id)
            for cell in cells :
                response = self.assignCell(cell, _worker)
                if not response.isOk() :
                    raise RuntimeError, str(response)

                self.__addCellToRouteTable( cell )

        ## scanDevices()

    def scanCells(self, path, dev_id) :
        cells = []

        if not os.path.isdir(path) :
            return cells

        table_dirs = os.listdir(path)
        for table_name in table_dirs :
            table_dir = os.sep.join((path, table_name))
            if not os.path.isdir(table_dir) :
                continue
            cell_dirs = os.listdir(table_dir)
            for cell_item in cell_dirs :
                cell_dir = os.sep.join((table_dir, cell_item))
                if not os.path.isdir(cell_dir) :
                    continue
                # check existence of cell config
                cell_configure = os.sep.join((cell_dir, CELL_CONFIG_FILE))
                if not os.path.exists(cell_configure) :
                    continue

                # load cell configure
                _table_name,_key_begin,_key_end = loadCellConfig(cell_configure)
                if _table_name != table_name :
                    raise RuntimeError, '%s configure table "%s"("%s" wanted).' % (cell_configure, _table_name, table_name)

                cell = Cell()
                cell.device = dev_id
                cell.path = cell_dir
                cell.table, cell.key_begin, cell.key_end = _table_name, _key_begin, _key_end

                cells.append( cell )

        return cells
        ## scanCells()

    def assignCell(self, cell, worker) :
        # STEP. make request
        request = Event()
        request.request_id = self.genRequestID()
        request.response_to = 0
        request.source_queue = self
        request.op = OP_ASSIGN_CELL
        request.body = cell

        # STEP. put request to event queue of cell
        reply = worker.handle(request)

        # STEP. wait for reply
        response = reply.body
        if response is None :
            # timeout
            response = Response()
            response.status_code = RC_TIMEOUT
            response.error = 'Timeout to wait reply for request "%s"' % str(request)

        return response
        ## assignCell()
    
    def lookupCell(self, table, key) :
        '''
        @param table name of table 
        @param key key of record

        @return response of operation
        '''
        response = Response()
        if table not in self._table_cells :
            response.status_code = RC_TABLE_NOT_FOUND
            response.error = 'Table "%s" does NOT exist' % table
            return response

        cells = self._table_cells[ table ]
        #self._logger.debug('cells:%s',repr(cells))
        if len(cells) < 1 :
            response.status_code = RC_CELL_NOT_FOUND
            response.error = 'No cell for table "%s"' % (table, )
            return response
        index = bisect_left(cells, (key,key,0,None))
        if index > 0 :
            cell = cells[index - 1]
            if (key > cell[0]) and (key <= cell[1]) :
                response.status_code = RC_OK
                response.body = cell[3]
                return response

        self.showRouteTable(table)

        response.status_code = RC_CELL_NOT_FOUND
        response.error = 'No cell of table "%s" charges key "%s"' % (table, key)
        return response
        ## lookupCell()

    def showRouteTable(self, table_name) :
        if table_name not in self._table_cells :
            return

        self._logger.info('==== Route Info of Table "%s" ====', table_name)
        cells = self._table_cells[ table_name ]
        for cell in cells :
            self._logger.info('  ( %s  .. %s ]', repr(cell[0]), repr(cell[1]))
        self._logger.info('====------------------------------------------------------====')
        ## showRouteTable()

    def lookupCellQueue(self, cell) :
        response = Response()
        
        self._logger.debug('cell:%s', repr(cell))
        if cell.device not in self._workers :
            response.status_code = RC_WORKER_NOT_FOUND
            response.error = 'No worker for device(id=%d path=%s)' % (cell.device, cell.path)
            return response
        worker = self._workers[ cell.device ]

        response.status_code = RC_OK
        response.body = worker

        return response
        ## lookupCellQueue()

    ####-----------------------------------------------------------####

    def existCell(self, table, key_begin, key_end) :
        '''
        test whether interval of cell is overlapped
        '''
        response = Response()
        if table not in self._table_cells :
            response.status_code = RC_TABLE_NOT_FOUND
            response.error = 'Table "%s" does NOT exist' % table
            return response

        cells = self._table_cells[ table ]
        index = bisect_left(cells, (key_begin,key_end,0,None))
        if index >= len(cells) :
            response.status_code = RC_CELL_NOT_FOUND
            response.error = 'No cell for tablet "%s (%s .. %s]"' % (table, key_begin, key_end)
            return response

        # TODO : join cells, and then check existence of cell
        cell = cells[index]
        if (key_begin == cell[0]) and (key_end == cell[1]) :
            response.status_code = RC_OK
            response.body = cell
            return response

        response.status_code = RC_CELL_NOT_FOUND
        return response
        ## existCell()

    def __existCellInRouteTable(self, table, key_begin, key_end) :
        '''
        test whether the exact cell exists. if exist, return it; otherwise, return None
        '''
        if table not in self._table_cells :
            return None

        cells = self._table_cells[ table ]
        index = bisect_left(cells, (key_begin,key_end,0,None))
        if index >= len(cells) :
            return None

        cell_tuple = cells[index]
        if (cell_tuple[0] == key_begin) and (cell_tuple[1] == key_end) :
            return cell_tuple[3]

        return None
        ## __existCellInRouteTable()

    def addCell(self, cell) :
        return self.__addCellToRouteTable( cell )
        ## addCell()

    def __addCellToRouteTable(self, cell) :
        if cell.table in self._table_cells :
            _cells = self._table_cells[ cell.table ]
        else :
            _cells = []
            self._table_cells[ cell.table ] = _cells
        insort( _cells, (cell.key_begin, cell.key_end, cell.device, cell) )
        ## __addCellToRouteTable()

    def __removeCellFromRouteTable(self, table, key_begin, key_end) :
        if table not in self._table_cells :
            return True

        cells = self._table_cells[ table ]
        index = bisect_left(cells, (key_begin,key_end,0,None))
        if index >= len(cells) :
            return True

        cell = cells[index]
        if (cell[0] == key_begin) and (cell[1] == key_end) :
            cells.pop( index )

        return True
        ## __removeCellFromRouteTable()

    def __getWorker(self, dev_id) :
        if dev_id in self._workers :
            _worker = self._workers[ dev_id ]
        else :
            _worker = Worker(self, dev_id)
            _worker.startup()
            self._workers[ dev_id ] = _worker

        return _worker
        ## __getWorker()

    ####-----------------------------------------------------------####

    def findAvailableDevice(self) :
        device = None
        max_available_space = 0

        for id,dev in self._devices.iteritems() :
            available_space = Device.getAvailableSpace( dev.path_list[0] )
            if available_space > max_available_space :
                max_available_space = available_space
                device = dev

        if device :
            if max_available_space < self._CELL_VOLUME :
                response = Response(status_code=RC_NOT_FOUND)
                response.error = 'No space available on device "%s".' % device.path_list[0]
            else :
                response = Response(status_code=RC_OK, body=device)
        else :
            response = Response(status_code=RC_NOT_FOUND)
            response.error = 'No device available.'

        return response
        ## findAvailableDevice()

    def waitReply(self, response_to) :
        reply = None
        while True :
            try :
                evt = self._queue_sync.get(True, TIMEOUT_WAIT_REPLY)
                if evt.response_to == response_to :
                    reply = evt.body
                    self._queue_sync.task_done()
                    break
                else :
                    self._queue_sync.task_done()
            except queue.Empty :
                return None

        return reply
        ## waitReply()

    ####-----------------------------------------------------------####

    def handle(self, evt) :
        self._logger.debug('KEEPER get event %s', str(evt))

        if ROUTE_ADD_CELL == evt.op :
            self.handleRouteAddCellEvent( evt )
        elif ROUTE_DEL_CELL == evt.op :
            self.handleRouteDelCellEvent( evt )
        elif EVENT_NEW_CELL_BORN == evt.op :
            self.handleNewCellBornEvent( evt )
        elif EVENT_NEW_CELL_DEVICE == evt.op :
            self.handleNewCellDeviceEvent( evt )

        else :
            raise RuntimeError,'Why not handle event "%s"' % str(evt)
        ## handle()

    def handleNewCellDeviceEvent(self, evt) :
        response = self.findAvailableDevice()
        if not response.isOk() :
            self._logger.error('fail to find new cell device : %s', response.error)
            return

        device = response.body
        evt.request_id = self.genRequestID()
        evt.op = OP_SPLIT_CELL
        table_name,db_path = evt.body
        evt.body = (table_name, db_path, device.dev_id, device.path_list[0])
        _worker = evt.source_queue
        evt.source_queue = self

        _worker.handle( evt )
        ## handleNewCellDeviceEvent()

    def handleNewCellBornEvent(self, evt) :
        table_name,db_path,key_begin,key_end = evt.body

        dev_id = Device.getDeviceID( db_path )
        cell = Cell()
        cell.device = dev_id
        cell.path = db_path
        cell.table,cell.key_begin,cell.key_end = table_name,key_begin,key_end

        worker = self.__getWorker( dev_id )

        response = self.assignCell(cell, worker)
        if not response.isOk() :
            self._logger.error('fail to assign cell "%s" : %s', str(cell), str(response))
        ## handleNewCellBornEvent()

    def handleRouteAddCellEvent(self, evt) :
        cells_list = evt.body

        for cell_tuple in cells_list :
            table_name,db_path,key_begin,key_end = cell_tuple

            if not self.__existCellInRouteTable(table_name, key_begin, key_end) :
                cell = Cell()
                cell.device = Device.getDeviceID( db_path )
                cell.path = db_path
                cell.table,cell.key_begin,cell.key_end = table_name,key_begin,key_end

                self.__addCellToRouteTable( cell )
        ## handleRouteAddCellEvent()

    def handleRouteDelCellEvent(self, evt) :
        cells_list = evt.body

        for cell_tuple in cells_list :
            table_name,db_path,key_begin,key_end = cell_tuple
            self.__removeCellFromRouteTable(table_name, key_begin, key_end)
        ## handleRouteDelCellEvent()

    ####-----------------------------------------------------------####

    def sumAvailableSpace(self) :
        sum_available_space = 0

        for id,dev in self._devices.iteritems() :
            available_space = Device.getAvailableSpace( dev.path_list[0] )
            sum_available_space += available_space

        return sum_available_space
        ## sumAvailableSpace()

    def setCellVolume(self, cell_volume) :
        # this method used for debugging mode
        if cell_volume > 128 * 1024 * 1024 :
            self._CELL_VOLUME = cell_volume
        ## setCellVolume()

    ## class Keeper

####---------------------------------------------------------------####

class   Device(object) :
    def __init__(self) :
        self.dev_id = 0
        self.path_list = []
        self.available_space = 0
        ## __init__()

    def getDeviceID(cls, path) :
        if not path : 
            return 0

        try :
            st = os.stat(path)
            dev_id = st[ stat.ST_DEV ]
            return dev_id
        except :
            # error
            return 0

        return 0
        ## getDeviceID()
    getDeviceID = classmethod(getDeviceID)

    def getAvailableSpace(cls, path) :
        if not path :
            return 0

        st = os.statvfs( path )
        if sys.version_info[:3] < (2,6,0) :
            block_size = st[statvfs.F_BSIZE]
            available_blocks = st[statvfs.F_BAVAIL]
        else :
            block_size = st.f_bsize
            available_blocks = st.f_bavail

        return block_size * available_blocks
        ## getAvailableSpace()
    getAvailableSpace = classmethod(getAvailableSpace)

    ## class Device

